package com.dbflow5.query;

import com.dbflow5.query.Operator.Operation;
import com.dbflow5.sql.Query;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

/**
 * Allows combining of [SQLOperator] into one condition.
 */
public class OperatorGroup extends BaseOperator implements Query, Iterable<SQLOperator> {

    private final List<SQLOperator> conditionsList = new ArrayList<>();

    private String internalQuery = null;
    private boolean isChanged;
    private boolean allCommaSeparated;
    private boolean useParenthesis = true;

    public List<SQLOperator> conditions() {
        return conditionsList;
    }

    private String querySafe() {
        return appendToQuery();
    }

    public OperatorGroup(NameAlias columnName) {
        super(columnName);
        // default is AND
        separator = Operation.AND;
    }

    /**
     * Will ignore all separators for the group and make them separated by comma. This is useful
     * in [Set] statements.
     *
     * @param allCommaSeparated All become comma separated.
     * @return This instance.
     */
    public OperatorGroup setAllCommaSeparated(boolean allCommaSeparated) {
        this.allCommaSeparated = allCommaSeparated;
        isChanged = true;
        return this;
    }

    /**
     * Sets whether we use paranthesis when grouping this within other [SQLOperator]. The default
     * is true, but if no conditions exist there are no paranthesis anyways.
     *
     * @param useParenthesis true if we use them, false if not.
     * @return This instance.
     */
    public OperatorGroup setUseParenthesis(boolean useParenthesis) {
        this.useParenthesis = useParenthesis;
        isChanged = true;
        return this;
    }

    /**
     * Appends the [SQLOperator] with an [Operation.OR]
     *
     * @param sqlOperator The condition to append.
     * @return This instance.
     */
    public OperatorGroup or(SQLOperator sqlOperator) {
        return operator(Operation.OR, sqlOperator);
    }

    /**
     * Appends the [SQLOperator] with an [Operation.AND]
     * @param sqlOperator sqlOperator
     * @return this
     */
    public OperatorGroup and(SQLOperator sqlOperator) {
        return operator(Operation.AND, sqlOperator);
    }

    /**
     * Applies the [Operation.AND] to all of the passed
     * [SQLOperator].
     * @param sqlOperators sqlOperators
     * @return this
     */
    public OperatorGroup andAll(SQLOperator... sqlOperators) {
        for(SQLOperator sqlOperator : sqlOperators) {
            and(sqlOperator);
        }
        return this;
    }

    /**
     * Applies the [Operation.AND] to all of the passed
     * [SQLOperator].
     * @param sqlOperators sqlOperators
     * @return this
     */
    public OperatorGroup andAll(Collection<SQLOperator> sqlOperators) {
        for(SQLOperator sqlOperator : sqlOperators) {
            and(sqlOperator);
        }
        return this;
    }

    /**
     * Applies the [Operation.AND] to all of the passed
     * [SQLOperator].
     * @param sqlOperators sqlOperators
     * @return this
     */
    public OperatorGroup orAll(SQLOperator... sqlOperators) {
        for(SQLOperator sqlOperator : sqlOperators) {
            or(sqlOperator);
        }
        return this;
    }

    /**
     * Applies the [Operation.AND] to all of the passed
     * [SQLOperator].
     * @param sqlOperators sqlOperators
     * @return this
     */
    public OperatorGroup orAll(Collection<SQLOperator> sqlOperators) {
        for(SQLOperator sqlOperator : sqlOperators) {
            or(sqlOperator);
        }
        return this;
    }

    /**
     * Appends the [SQLOperator] with the specified operator string.
     * @param operator operator
     * @param sqlOperator sqlOperator
     * @return this
     */
    public OperatorGroup operator(String operator, SQLOperator sqlOperator) {
        if (sqlOperator != null) {
            setPreviousSeparator(operator);
            conditionsList.add(sqlOperator);
            isChanged = true;
        }
        return this;
    }

    public void appendConditionToQuery(StringBuilder queryBuilder) {
        int conditionListSize = conditionsList.size();
        if (useParenthesis && conditionListSize > 0) {
            queryBuilder.append("(");
        }
        for (int i = 0; i < conditionListSize; i++) {
            SQLOperator condition = conditionsList.get(i);
            condition.appendConditionToQuery(queryBuilder);
            if (!allCommaSeparated && condition.hasSeparator() && i < conditionListSize - 1) {
                queryBuilder.append(" ").append(condition.separator()).append(" ");
            } else if (i < conditionListSize - 1) {
                queryBuilder.append(", ");
            }
        }
        if (useParenthesis && conditionListSize > 0) {
            queryBuilder.append(")");
        }
    }

    /**
     * Sets the last condition to use the separator specified
     *
     * @param separator AND, OR, etc.
     */
    private void setPreviousSeparator(String separator) {
        if (conditionsList.size() > 0) {
            // set previous to use OR separator
            conditionsList.get(conditionsList.size() - 1).separator(separator);
        }
    }

    @Override
    public String getQuery() {
        if (isChanged) {
            internalQuery = querySafe();
        }
        return internalQuery == null ? "" : internalQuery;
    }

    @Override
    public String toString() {
        return querySafe();
    }

    public int size() {
        return conditionsList.size();
    }

    @Override
    public Iterator<SQLOperator> iterator() {
        return conditionsList.iterator();
    }

    /**
     * Starts an arbitrary clause
     * @return Starts an arbitrary clause of conditions to use.
     */
    public static OperatorGroup clause() {
        return new OperatorGroup(null);
    }

    /**
     * Starts an arbitrary clause of conditions
     * @param condition condition
     * @return Starts an arbitrary clause of conditions to use with first param as conditions separated by AND.
     */
    public static OperatorGroup clause(SQLOperator... condition) {
        return new OperatorGroup(null).andAll(condition);
    }

    /**
     * Starts an arbitrary clause of conditions to use
     * @return Starts an arbitrary clause of conditions to use, that when included in other [SQLOperator],
     * does not append parenthesis to group it.
     */
    public static OperatorGroup nonGroupingClause() {
        return new OperatorGroup(null).setUseParenthesis(false);
    }

    /**
     * Starts an arbitrary clause of conditions
     * @param condition condition
     * @return Starts an arbitrary clause of conditions (without parenthesis) to use with first param as conditions separated by AND.
     */
    public static OperatorGroup nonGroupingClause(SQLOperator... condition) {
        return new OperatorGroup(null).setUseParenthesis(false).andAll(condition);
    }

//    public OperatorGroup and(SQLOperator sqlOperator) {
//        return and(sqlOperator);
//    }
//
//    public OperatorGroup or(SQLOperator sqlOperator) {
//        return or(sqlOperator);
//    }
//
//    public OperatorGroup and(OperatorGroup sqlOperator) {
//        return clause().and(sqlOperator);
//    }
//
//    public OperatorGroup or(OperatorGroup sqlOperator) {
//        return clause().or(sqlOperator);
//    }
}

